﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
using DeepEarth.Core;
using DeepEarth.Core.Controls;
using DeepEarth.Core.Utilities;
using DeepEarth.Map.Core;
using DeepEarth.Map.Core.Tiling;
using DeepEarth.Map.Core.Utilities;
using DeepEarth.Map.TileSources;
using bing = Microsoft.Maps.MapControl;

namespace DeepEarth.Map.BingMaps
{
    [ContentProperty("Children")]
    public class BingMap : Microsoft.Maps.MapControl.Map, IMap
    {
        const int DEFAULT_PRECISION = 10;

        #region Bing Events
        IDisposable viewChangeSubscription = null;
        bool listenToViewChanges = true;

        void BingMap_ViewChangeOnFrame(object sender, bing.MapEventArgs e)
        {
            if (ViewChangeOnFrame != null)
            {
                ViewChangeOnFrame(this, e);
            }            
        }

        void BingMap_ViewChangeEnd(object sender, bing.MapEventArgs e)
        {
            if (ViewChangeEnd != null)
            {
                ViewChangeEnd(this, e);
            }
        }

        void BingMap_TargetViewChanged(object sender, bing.MapEventArgs e)
        {
            var syncRequired = false;
            var center = View.Center;

            if (View != null && !TargetCenter.EqualsDE(View.Center,10))
            {
                // Don't propagate invalid targets
                if (!double.IsNaN(TargetCenter.Longitude) && !double.IsNaN(TargetCenter.Latitude) && !double.IsNaN(TargetCenter.Altitude))
                {
                    syncRequired = true;
                    center = BingLocationConverter.ConvertToDeepEarth(TargetCenter);
                }
            }

            var viewportWidth = MapView.ViewportWidthFromZoomLevel(ActualWidth,base.TargetZoomLevel);
            if (View != null && Math.Round(View.ViewportWidth, DEFAULT_PRECISION) != Math.Round(viewportWidth, DEFAULT_PRECISION))
            {
                syncRequired = true;
            }

            // In the deepearth map control, the map reacts to view changes. In the bing maps wrapper, the map can react to view changes, or it may be
            // changing due to its own interaction handlers - in that case, we synchronize the view to the map
            if (syncRequired)
            {
                listenToViewChanges = false;
                View.SetLocation(center, viewportWidth);
                listenToViewChanges = true;
            }
        }

        void BingMap_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            base.AnimationLevel = bing.AnimationLevel.None;

            var newLocation = BingLocationConverter.ConvertToBing(View.Center);
            var newZoom = MapView.ZoomLevelFromViewportWidth(ActualWidth, View.ViewportWidth);

            Center = newLocation;
            ZoomLevel = newZoom;

            base.AnimationLevel = bing.AnimationLevel.Full;
        }
        #endregion

        Layer baseLayer;
        LayerGroup baseLayerGroup;

        public BingMap()
        {
            Panel overlayPanel;
            Panel interactionSurface;
            var baselayers = MapHelper.InitializeSurfaces(this, true, base.Children, out baseLayerGroup, out baseLayer, out overlayPanel, out interactionSurface);

            OverlayPanel = overlayPanel;
            InteractionSurface = interactionSurface;

            BaseRasterOverlayCollection = baselayers.Item1;
            BaseDynamicLayer = baselayers.Item2;
            BaseVectorLayerCollection = baselayers.Item3;
            BaseLabelLayer = baselayers.Item4;

            View = new MapView(new BingMapTileSourceProvider(null));

            base.TargetViewChanged += BingMap_TargetViewChanged;
            base.ViewChangeOnFrame += BingMap_ViewChangeOnFrame;
            base.ViewChangeEnd += BingMap_ViewChangeEnd;
            SizeChanged += BingMap_SizeChanged;

            NavigateCommand = new LambdaCommand<object>(o =>
            {
                if (o is ILocatable)
                {
                    var locatable = o as ILocatable;
                    if (locatable.Position != null)
                    {
                        View.SetLocation(locatable.Position, View.ViewportWidth);
                    }
                }
            });
        }

        #region Properties

        public new Layer Children
        {
            get { return baseLayer; }
        }

        public UIElementCollection BaseChildren
        {
            get { return base.Children; }
        }

        public new bing.MapLayer RootLayer
        {
            get { return base.RootLayer; }
        }

        public Control ScaleElement
        {
            get { return this.Descendants().OfType<Control>().FirstOrDefault(c => c.Name == "ScaleElement"); }
        }
        #endregion

        #region IMap
        public bool IsWrapped
        {
            get { return true; }
        }

        bool isInteractive = true;
        public bool IsInteractive
        {
            get { return isInteractive; }
            set { isInteractive = value; }
        }

        public DependencyProperty ViewDependencyProperty
        {
            get { return ViewProperty; }
        }

        public string ViewDependencyPropertyPath
        {
            get { return "View"; }
        }

        public MapView View
        {
            get { return (MapView)GetValue(ViewProperty); }
            set { SetValue(ViewProperty, value); }
        }

        public FrameworkElement AsFrameworkElement
        {
            get { return this; }
        }

        double IMap.ZoomLevel
        {
            get { return TargetZoomLevel; }
            set { ZoomLevel = value; }
        }

        public void Pan(double deltaX, double deltaY)
        {
            var pixelCenter = new Point(ActualWidth / 2.0, ActualHeight / 2.0);
            var newPixelCenter = new Point(pixelCenter.X + deltaX, pixelCenter.Y + deltaY);
            base.SetView(base.ViewportPointToLocation(newPixelCenter), ZoomLevel);
        }

        public void Zoom(double delta)
        {
            if (delta > 0)
                ZoomLevel++;
            else
                ZoomLevel--;
        }

        public static readonly DependencyProperty TileSourceProviderProperty = DependencyProperty.Register("TileSourceProvider", typeof(BaseTileSourceProvider), typeof(BingMap), new PropertyMetadata(TileSourceProviderPropertyChanged));
        public BaseTileSourceProvider TileSourceProvider
        {
            get { return (BaseTileSourceProvider)GetValue(TileSourceProviderProperty); }
            set { SetValue(TileSourceProviderProperty, value); }
        }

        static void TileSourceProviderPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (BingMap)sender;

            if (args.NewValue != null && self.View != null)
            {
                self.View.TileSourceProvider = args.NewValue as BaseTileSourceProvider;
            }
        }

        public new Location ViewportPointToLocation(Point point)
        {
            return BingLocationConverter.ConvertToDeepEarth(base.ViewportPointToLocation(point));
        }

        public Point LocationToViewportPoint(Location location)
        {
            Point p = base.LocationToViewportPoint(BingLocationConverter.ConvertToBing(location));

            return p;
        }

        public new event EventHandler ViewChangeOnFrame;
        public new event EventHandler ViewChangeEnd;

        public Location CurrentCenter
        {
            get { return BingLocationConverter.ConvertToDeepEarth(Center); }
        }

        public double CurrentViewportLogicalWidth
        {
            get { return MapView.ViewportWidthFromZoomLevel(ActualWidth, ZoomLevel); }
        }

        public IMap CreateInstance()
        {
            return new BingMap
            {
                CredentialsProvider = CredentialsProvider,
                NavigationVisibility = Visibility.Collapsed,
                CopyrightVisibility = Visibility.Collapsed,
                LogoVisibility = Visibility.Collapsed,
                ScaleVisibility = Visibility.Collapsed
            };
        }

        public Panel OverlayPanel { get; set; }

        public RasterOverlayCollection BaseRasterOverlayCollection { get; private set; }
        public Canvas BaseDynamicLayer { get; private set; }
        public VectorLayerCollection BaseVectorLayerCollection { get; private set; }
        public Canvas BaseLabelLayer { get; private set; }

        public Panel InteractionSurface { get; set; }

        public bool applyDefaultNavigationCommand = true;
        public bool ApplyDefaultNavigationCommand
        {
            get { return applyDefaultNavigationCommand; }
            set { applyDefaultNavigationCommand = value; }
        }

        public static readonly DependencyProperty NavigateCommandProperty = DependencyProperty.Register("NavigateCommand", typeof(ICommand), typeof(BingMap), null);
        public ICommand NavigateCommand
        {
            get { return (ICommand)GetValue(NavigateCommandProperty); }
            set { SetValue(NavigateCommandProperty, value); }
        }

        bool disableZoom;
        public bool DisableZoom
        {
            get { return disableZoom; }
            set
            {
                disableZoom = value;

                if (value)
                {
                    base.MouseDragBox += (o, e) => e.Handled = true;
                    base.MouseDoubleClick += (o, e) => e.Handled = true;
                    base.MouseWheel += (o, e) => e.Handled = true;
                }
            }
        }

        public AnimationLevel AnimationLevel
        {
            get
            {
                if (base.AnimationLevel == bing.AnimationLevel.None)
                    return AnimationLevel.None;

                return AnimationLevel.Full;
            }
            set
            {
                if (value == AnimationLevel.None)
                    base.AnimationLevel = bing.AnimationLevel.None;
                else
                    base.AnimationLevel = bing.AnimationLevel.Full;
            }
        }

        #endregion

        #region Bing Map Overrides
        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (IsInteractive)
                base.OnMouseMove(e);
        }
        #endregion

        #region IGeoItemsContainer
        TemplateSelector templateSelector;
        public TemplateSelector TemplateSelector
        {
            get
            {
                return templateSelector;
            }
            set
            {
                templateSelector = value;
                baseLayerGroup.TemplateSelector = value;
            }
        }

        TemplateSelector labelTemplateSelector;
        public TemplateSelector LabelTemplateSelector
        {
            get
            {
                return labelTemplateSelector;
            }
            set
            {
                labelTemplateSelector = value;
                baseLayerGroup.LabelTemplateSelector = value;
            }
        }

        public GeoItems GeoItems
        {
            get { return (GeoItems)GetValue(GeoItemsProperty); }
            set { SetValue(GeoItemsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for GeoItems.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GeoItemsProperty =
            DependencyProperty.Register("GeoItems", typeof(GeoItems), typeof(BingMap), new PropertyMetadata(GeoItemsPropertyChanged));

        private static void GeoItemsPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (BingMap)sender;

            if (args.NewValue != null)
            {
                var geoItems = (GeoItems)args.NewValue;

                self.baseLayerGroup.ItemsSource = geoItems.ViewModels;

                if (self.ApplyDefaultNavigationCommand)
                {
                    geoItems.NavigateCommand = MultiCommand.Combine(geoItems.NavigateCommand, self.NavigateCommand);
                }
            }
        }
        #endregion

        public static readonly DependencyProperty ViewProperty = DependencyProperty.Register("View", typeof(MapView), typeof(BingMap), new PropertyMetadata(CurrentViewChanged));

        static void CurrentViewChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (BingMap)sender;

            if (args.OldValue != null)
            {
                if (self.viewChangeSubscription != null)
                {
                    self.viewChangeSubscription.Dispose();
                }
            }

            if (args.NewValue != null)
            {
                var newView = args.NewValue as MapView;

                var observableProperties = Observable.FromEventPattern<PropertyChangedEventArgs>(newView, "PropertyChanged").Where(i => self.listenToViewChanges);
                self.viewChangeSubscription = observableProperties.Subscribe(e => self.HandleViewPropertyChanged(e.EventArgs));
            }
        }

        void HandleViewPropertyChanged(PropertyChangedEventArgs args)
        {
            var newLocation = BingLocationConverter.ConvertToBing(View.Center);
            var newZoom = MapView.ZoomLevelFromViewportWidth(ActualWidth, View.ViewportWidth);

            if (args.PropertyName != MapView.TILE_SOURCE_PROVIDER && args.PropertyName != MapView.PROVIDER_TILESOURCES)
            {
                if (TargetCenter == newLocation && TargetZoomLevel == newZoom)
                {
                    return;
                }
            }

            switch (args.PropertyName)
            {
                case MapView.CENTER :
                    
                    if (!TargetCenter.EqualsDE(View.Center,10))
                    {
                        Center = newLocation;
                        ZoomLevel = newZoom;
                    }
                    break;

                case MapView.VIEWPORT_WIDTH :
                case MapView.ZOOM_LEVEL :
                    ZoomLevel = newZoom;
                    break;
                case MapView.TILE_SOURCE_PROVIDER :
                case MapView.PROVIDER_TILESOURCES :
                    if (View.TileSourceProvider.GetType() == typeof(BingMapTileSourceProvider))
                    {
                        var bingMode = (BingMapTileSourceProvider)View.TileSourceProvider;
                        base.Mode = BingMapModeFactory.CreateBingMapMode(bingMode.Mode);
                    }
                    if (View.TileSourceProvider.GetType() == typeof(QuadKeyTileSourceProvider))
                    {                        
                        base.Mode = new CustomMode((QuadKeyTileSourceProvider)View.TileSourceProvider);
                    }
                    break;
            }

        }

        public new LocationRectangle BoundingRectangle
        {
            get
            {
                var baseRect = base.BoundingRectangle;

                var west = baseRect.Northwest.Longitude;
                var east = baseRect.Southeast.Longitude;

                if (baseRect.Width >= 360)
                {
                    west = -180;
                    east = 180;
                }

                return new LocationRectangle(new Location(baseRect.Northwest.Latitude, west), new Location(baseRect.Southeast.Latitude, east));
            }
        }
    }

    internal static class Extensions
    {
        internal static bool EqualsDE(this Microsoft.Maps.MapControl.Location bingLocation, Location deLocation, int precision)
        {
            // If both values are NaN, treat as equal.
            return
                (Math.Round(bingLocation.Altitude, precision) == Math.Round(deLocation.Altitude, precision) || (double.IsNaN(bingLocation.Altitude) && double.IsNaN(deLocation.Altitude))) &&
                (Math.Round(bingLocation.Longitude, precision) == Math.Round(deLocation.Longitude,precision) || (double.IsNaN(bingLocation.Longitude) && double.IsNaN(deLocation.Longitude))) &&
                (Math.Round(bingLocation.Latitude, precision) == Math.Round(deLocation.Latitude, precision) || (double.IsNaN(bingLocation.Latitude) && double.IsNaN(deLocation.Latitude)));
        }
    }
}
